Skip to content

Add configurable tab title typography#35

Open
alexgorbatchev wants to merge 2 commits intomanaflow-ai:mainfrom
alexgorbatchev:ui-font-api
Open

Add configurable tab title typography#35
alexgorbatchev wants to merge 2 commits intomanaflow-ai:mainfrom
alexgorbatchev:ui-font-api

Conversation

@alexgorbatchev
Copy link
Copy Markdown

@alexgorbatchev alexgorbatchev commented Mar 24, 2026

Add configurable tab title typography

Summary

Add configurable pane tab title typography to Bonsplit so host apps can override tab title family and scale without changing the default system font behavior.

Why

Host apps need a supported way to style pane tab titles to match their UI typography. Bonsplit previously hardcoded the system font for tab titles and drag previews, which made host-level font customization impossible without patching internal views.

Changes

  • add tabTitleFontFamily and tabTitleFontScale to BonsplitConfiguration.Appearance
  • route pane tab titles and drag previews through a shared TabBarTypography helper instead of hardcoded system font calls
  • resolve requested font families through AppKit and reject silent fallbacks that return the system font instead of the requested family
  • add tests covering default system-font behavior, override storage, point-size scaling, and requested-family resolution

Validation

  • swift test

Summary by cubic

Adds configurable typography for pane tab titles so host apps can set a font family and scale while keeping the system font as the default. Updates tab titles and drag previews to use a shared resolver and hardens scale handling.

  • New Features
    • BonsplitConfiguration.Appearance adds tabTitleFontFamily and tabTitleFontScale.
    • New TabBarTypography resolves fonts via AppKit, enforces the requested family, clamps scale to 0.5+, and falls back to the system font when missing or non-finite.
    • Tab titles and drag previews now use TabBarTypography.
    • Tests cover defaults, overrides, scaling (including non-finite), and family resolution.

Written for commit 0d0ce6e. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Added appearance options to customize tab bar title typography: selectable font family and adjustable scale.
    • Tab bar titles now use the configured typography and scale instead of a fixed font size.
  • Tests

    • Added tests covering typography configuration, font resolution, scaling behavior, and handling of invalid scale values.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

Introduces TabBarTypography to derive SwiftUI fonts for tab titles from BonsplitConfiguration.Appearance (tabTitleFontFamily, tabTitleFontScale). Replaces fixed title fonts in tab views with the new API and adds tests for font resolution, scaling, and non-finite scale handling.

Changes

Cohort / File(s) Summary
Tab Bar Typography System
Sources/Bonsplit/Internal/Styling/TabBarTypography.swift
New enum with titleFont(for:) and resolvedTitleNSFont(for:). Resolves an NSFont via family/name strategies, maps NSFont.Weight to font-manager weights, clamps scale (minimum 0.5), treats non-finite scales as default, and converts to SwiftUI Font.
Configuration Extension
Sources/Bonsplit/Public/BonsplitConfiguration.swift
Added Appearance properties tabTitleFontFamily: String? and tabTitleFontScale: CGFloat (defaults nil and 1.0) and updated initializer to accept them.
View Integration
Sources/Bonsplit/Internal/Views/TabDragPreview.swift, Sources/Bonsplit/Internal/Views/TabItemView.swift
Replaced fixed .system(size: TabBarMetrics.titleFontSize) font modifiers with .font(TabBarTypography.titleFont(for: appearance)), making title rendering driven by appearance.
Test Coverage
Tests/BonsplitTests/BonsplitTests.swift
Added helper to pick an available font family and five @MainActor tests covering default appearance, system-font fallback, override persistence, linear scaling behavior, and handling of non-finite tabTitleFontScale values.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A little rabbit nibbling type and space,
I hop through fonts with gentle, jaunty pace.
Families and scales in tidy rows align,
Tab titles bloom in form and size divine,
A fluttering tweak — now every tab can grace. 🎋

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding configurable tab title typography. It accurately reflects the primary functionality—exposing tabTitleFontFamily and tabTitleFontScale to allow host apps to customize tab title styling without hardcoding fonts.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 24, 2026

Greptile Summary

This PR adds two new BonsplitConfiguration.Appearance properties — tabTitleFontFamily and tabTitleFontScale — and routes all tab bar title rendering (both live tabs and drag previews) through a new TabBarTypography helper instead of hardcoded .system font calls, giving host apps a supported way to match their own typography.

  • TabBarTypography uses a three-stage AppKit lookup to resolve a requested family (via NSFontManager, then a descriptor fallback, then a PostScript-name fallback) and explicitly rejects silent system-font substitution via fontMatchesRequestedFamily
  • Both TabItemView and TabDragPreview now call TabBarTypography.titleFont(for: appearance) — one-line changes with no other side effects
  • BonsplitConfiguration.Appearance gains the two new properties with backwards-compatible defaults (nil / 1.0), preserving existing Sendable conformance
  • Five new tests cover defaults, storage, scale arithmetic, and live family resolution with a machine-aware XCTSkip guard
  • The descriptor-based intermediate lookup path in TabBarTypography appears to be effectively unreachable in practice (system-font private attributes override the family attribute), which slightly increases code complexity without adding coverage
  • The tabTitleFontScale doc-comment does not document the silent 0.5 minimum clamp enforced inside resolvedTitleNSFont

Confidence Score: 5/5

  • Safe to merge — changes are additive, backwards-compatible, and covered by tests; no runtime errors or data-loss risks.
  • The two new Appearance fields default to the exact existing behaviour (nil / 1.0), so no current host is affected. The font-resolution logic guards against the primary AppKit footgun (silent system-font substitution) explicitly. The two flagged issues are both P2 style suggestions (a missing doc note about the 0.5 floor, and a likely-dead intermediate lookup branch) — neither affects correctness or reliability in normal usage.
  • No files require special attention beyond the minor documentation gap in BonsplitConfiguration.swift.

Important Files Changed

Filename Overview
Sources/Bonsplit/Internal/Styling/TabBarTypography.swift New helper enum that centralises font resolution for tab bar titles. Logic is sound — triple-lookup chain guards against silent system-font fallbacks. One path (descriptor-based) appears effectively unreachable in practice.
Sources/Bonsplit/Public/BonsplitConfiguration.swift Cleanly adds two new Appearance properties with sensible defaults. The tabTitleFontScale doc-comment omits the effective minimum of 0.5 that is enforced internally.
Sources/Bonsplit/Internal/Views/TabDragPreview.swift One-line font swap from hardcoded .system to TabBarTypography.titleFont. Change is minimal and correct.
Sources/Bonsplit/Internal/Views/TabItemView.swift Same one-line font swap as TabDragPreview; consistent and correct.
Tests/BonsplitTests/BonsplitTests.swift Adds five well-scoped tests covering defaults, storage, scale, and family resolution. preferredTabTitleTestFontFamily uses XCTSkip gracefully when no test font is available.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["TabItemView / TabDragPreview\n.font(TabBarTypography.titleFont(for: appearance))"] --> B["TabBarTypography.titleFont"]
    B --> C["resolvedTitleNSFont\nscaledSize = max(0.5, scale) × titleFontSize"]
    C --> D{"tabTitleFontFamily\nset?"}
    D -- "nil / empty" --> E["NSFont.systemFont(ofSize: scaledSize)"]
    D -- "non-empty" --> F["resolvedFont(family:size:weight:)"]
    F --> G["resolvedPostScriptFontName"]
    G --> H["NSFontManager.shared.font(withFamily:traits:weight:size:)"]
    H --> I{"fontMatchesRequestedFamily?"}
    I -- "✓ match" --> J["return fontName"]
    I -- "✗ no match" --> K["NSFont(descriptor: systemDescriptor.withFamily(family), size:)"]
    K --> L{"fontMatchesRequestedFamily?"}
    L -- "✓ match" --> J
    L -- "✗ no match" --> M["NSFont(name: family, size:)"]
    M --> N{"familyName match\nOR postScript name match?"}
    N -- "✓" --> J
    N -- "✗" --> O["return nil"]
    O --> E
    J --> P["NSFont(name: postScriptName, size:)"]
    P --> Q["Font(nsFont)\n→ SwiftUI Font"]
    E --> Q
Loading

Reviews (1): Last reviewed commit: "Add configurable tab title typography" | Re-trigger Greptile

Comment on lines +193 to +194
/// `1.0` preserves the current size.
public var tabTitleFontScale: CGFloat
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing lower-bound documentation for tabTitleFontScale

The implementation in TabBarTypography.resolvedTitleNSFont silently clamps the scale to a minimum of 0.5 via max(0.5, appearance.tabTitleFontScale). The public doc-comment for this property doesn't mention this floor, so a host app passing 0.1 will be surprised that the effective minimum is 0.5.

Suggested change
/// `1.0` preserves the current size.
public var tabTitleFontScale: CGFloat
/// Multiplier applied to pane tab title size.
/// `1.0` preserves the current size. Values below `0.5` are clamped to `0.5`.
public var tabTitleFontScale: CGFloat

Comment on lines +46 to +52

let systemDescriptor = NSFont.systemFont(ofSize: size, weight: weight).fontDescriptor
let familyDescriptor = systemDescriptor.withFamily(family)
if let font = NSFont(descriptor: familyDescriptor, size: size),
fontMatchesRequestedFamily(font, family: family) {
return font.fontName
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Second lookup path likely never succeeds

The intermediate lookup builds a new NSFontDescriptor by calling systemDescriptor.withFamily(family). withFamily only overrides the NSFontFamilyAttribute key, but the system font descriptor carries private San Francisco–specific attributes (e.g., NSCTFontUIUsageAttribute) that rank above a plain family match during font matching. In practice, NSFont(descriptor: familyDescriptor, size:) will almost always return another San Francisco variant rather than the requested family, which is then caught and rejected by fontMatchesRequestedFamily. As a result this branch is effectively dead code and adds confusion without contributing coverage.

Consider removing it so the fallback path is a cleaner: first NSFontManager, then NSFont(name:size:).

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/Bonsplit/Internal/Styling/TabBarTypography.swift`:
- Around line 10-15: Sanitize appearance.tabTitleFontScale before computing
scaledSize in TabBarTypography: read the raw scale into a local (e.g., let
rawScale = appearance.tabTitleFontScale), if rawScale.isFinite is false then
replace it with a sane default (e.g., 1.0), then compute let scaledSize =
max(0.5, sanitizedScale) * TabBarMetrics.titleFontSize and proceed to call
resolvedFont(...) ?? NSFont.systemFont(ofSize: scaledSize); update any relevant
tests for resolvedFont/tabTitleFontScale to include NaN and +∞ inputs to ensure
the guard works.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: af1c5463-83e8-4a57-9cd4-a3bcfea90585

📥 Commits

Reviewing files that changed from the base of the PR and between 1610b45 and c077265.

📒 Files selected for processing (5)
  • Sources/Bonsplit/Internal/Styling/TabBarTypography.swift
  • Sources/Bonsplit/Internal/Views/TabDragPreview.swift
  • Sources/Bonsplit/Internal/Views/TabItemView.swift
  • Sources/Bonsplit/Public/BonsplitConfiguration.swift
  • Tests/BonsplitTests/BonsplitTests.swift

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 5 files


Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
Sources/Bonsplit/Internal/Styling/TabBarTypography.swift (1)

66-88: Consider replacing raw weight integers with named constants/comments.

The mapping is fine, but documenting the NSFontManager weight scale (or using local constants) would make this safer to maintain.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Bonsplit/Internal/Styling/TabBarTypography.swift` around lines 66 -
88, The switch in private static func fontManagerWeight(for weight:
NSFont.Weight) currently returns raw integers (e.g., 2,3,4...) which are unclear
and fragile; replace those magic numbers with named constants or an internal
enum (e.g., FONT_WEIGHT_ULTRA_LIGHT, FONT_WEIGHT_THIN, etc.) or add an inline
comment referencing the NSFontManager weight scale to document each mapping,
then update the return values to use those constants (references:
fontManagerWeight and the NSFont.Weight cases .ultraLight, .thin, .light,
.regular, .medium, .semibold, .bold, .heavy, .black).
Sources/Bonsplit/Public/BonsplitConfiguration.swift (1)

192-194: Document non-finite scale behavior in the public API docs.

Line 193 documents clamping for values below 0.5, but runtime logic also normalizes non-finite values. Adding that note here would reduce host-app surprises.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Bonsplit/Public/BonsplitConfiguration.swift` around lines 192 - 194,
Update the public doc comment for the BonsplitConfiguration.tabTitleFontScale
property to mention that values below 0.5 are clamped to 0.5 and that non-finite
inputs (NaN, +inf, -inf) are normalized/handled at runtime (e.g., converted to a
safe finite value or defaulted) to avoid surprises for host apps; locate the
comment above the tabTitleFontScale declaration and add a concise sentence
specifying the exact non‑finite behavior and resulting value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Sources/Bonsplit/Internal/Styling/TabBarTypography.swift`:
- Around line 66-88: The switch in private static func fontManagerWeight(for
weight: NSFont.Weight) currently returns raw integers (e.g., 2,3,4...) which are
unclear and fragile; replace those magic numbers with named constants or an
internal enum (e.g., FONT_WEIGHT_ULTRA_LIGHT, FONT_WEIGHT_THIN, etc.) or add an
inline comment referencing the NSFontManager weight scale to document each
mapping, then update the return values to use those constants (references:
fontManagerWeight and the NSFont.Weight cases .ultraLight, .thin, .light,
.regular, .medium, .semibold, .bold, .heavy, .black).

In `@Sources/Bonsplit/Public/BonsplitConfiguration.swift`:
- Around line 192-194: Update the public doc comment for the
BonsplitConfiguration.tabTitleFontScale property to mention that values below
0.5 are clamped to 0.5 and that non-finite inputs (NaN, +inf, -inf) are
normalized/handled at runtime (e.g., converted to a safe finite value or
defaulted) to avoid surprises for host apps; locate the comment above the
tabTitleFontScale declaration and add a concise sentence specifying the exact
non‑finite behavior and resulting value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3acabe68-9a5f-4a56-9bf4-24da3038e343

📥 Commits

Reviewing files that changed from the base of the PR and between c077265 and 0d0ce6e.

📒 Files selected for processing (3)
  • Sources/Bonsplit/Internal/Styling/TabBarTypography.swift
  • Sources/Bonsplit/Public/BonsplitConfiguration.swift
  • Tests/BonsplitTests/BonsplitTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Tests/BonsplitTests/BonsplitTests.swift

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant